<?php

/**
* 实现数据库的主从分离，该类会维护多个数据库的配置：一个主数据库配置，多个从数据库的配置。
* 具体使用主数据库还是从数据库，使用如下规则：
* 1、CDbCommandExt的prepare方法会根据sql语句是读还是写，来调用CDbConnectionExt的getPdoInstance方法，来获取主数据库或者从数据库链接，默认使用主数据库
* 2、如果当前处于一个事务中，那么无视第一条规则，在事务结束前全部使用主数据库
* 3、如果从数据库的配置为空，则使用主数据库
*/
class CDbConnectionExt extends CDbConnection
{
    /**
    * @var string The Data Source Name, or DSN, contains the information required to connect to the database.
    * @see http://www.php.net/m...O-construct.php
    */
    public $connectionString;
    /**
    * @var string the username for establishing DB connection. Defaults to empty string.
    */
    public $username='';
    /**
    * @var string the password for establishing DB connection. Defaults to empty string.
    */
    public $password='';
    
    /**
    * @var array 从数据库配置数组，例如：
    * array(
    * array('connectionString'=>'mysql:host=192.168.1.100;dbname=McAm;port=8001', 'username'=>'mcam', 'password'=>'123456'),
    * array('connectionString'=>'mysql:host=192.168.1.101;dbname=McAm;port=8001', 'username'=>'mcam', 'password'=>'123456'),
    * array('connectionString'=>'mysql:host=192.168.1.102;dbname=McAm;port=8001', 'username'=>'mcam', 'password'=>'123456'),
    * )
    */
    public $slaveConfig = array();
    
    /**
    * @var integer number of seconds that table metadata can remain valid in cache.
    * Use 0 or negative value to indicate not caching schema.
    * If greater than 0 and the primary cache is enabled, the table metadata will be cached.
    * @see schemaCachingExclude
    */
    public $schemaCachingDuration=0;
    /**
    * @var array list of tables whose metadata should NOT be cached. Defaults to empty array.
    * @see schemaCachingDuration
    */
    public $schemaCachingExclude=array();
    /**
    * @var string the ID of the cache application component that is used to cache the table metadata.
    * Defaults to 'cache' which refers to the primary cache application component.
    * Set this property to false if you want to disable caching table metadata.
    * @since 1.0.10
    */
    public $schemaCacheID='cache';
    /**
    * @var boolean whether the database connection should be automatically established
    * the component is being initialized. Defaults to true. Note, this property is only
    * effective when the CDbConnection object is used as an application component.
    */
    public $autoConnect=true;
    /**
    * @var string the charset used for database connection. The property is only used
    * for MySQL and PostgreSQL databases. Defaults to null, meaning using default charset
    * as specified by the database.
    */
    public $charset;
    /**
    * @var boolean whether to turn on prepare emulation. Defaults to false, meaning PDO
    * will use the native prepare support if available. For some databases (such as MySQL),
    * this may need to be set true so that PDO can emulate the prepare support to bypass
    * the buggy native prepare support. Note, this property is only effective for PHP 5.1.3 or above.
    */
    public $emulatePrepare=false;
    /**
    * @var boolean whether to log the values that are bound to a prepare SQL statement.
    * Defaults to false. During development, you may consider setting this property to true
    * so that parameter values bound to SQL statements are logged for debugging purpose.
    * You should be aware that logging parameter values could be expensive and have significant
    * impact on the performance of your application.
    * @since 1.0.5
    */
    public $enableParamLogging=false;
    /**
    * @var boolean whether to enable profiling the SQL statements being executed.
    * Defaults to false. This should be mainly enabled and used during development
    * to find out the bottleneck of SQL executions.
    * @since 1.0.6
    */
    public $enableProfiling=false;
    /**
    * @var string the default prefix for table names. Defaults to null, meaning no table prefix.
    * By setting this property, any token like '{{tableName}}' in {@link CDbCommand::text} will
    * be replaced by 'prefixTableName', where 'prefix' refers to this property value.
    * @since 1.1.0
    */
    public $tablePrefix;
    /**
    * @var array list of SQL statements that should be executed right after the DB connection is established.
    * @since 1.1.1
    */
    public $initSQLs;
    
    private $_attributes=array();
    private $_active=false;
    private $_pdo;
    private $_transaction;
    private $_schema;
    
    //主数据库、从数据库链接
    private $_pdoMaster = null;
    private $_pdoSlave = null;
    
    /**
    * Constructor.
    * Note, the DB connection is not established when this connection
    * instance is created. Set {@link setActive active} property to true
    * to establish the connection.
    * @param string The Data Source Name, or DSN, contains the information required to connect to the database.
    * @param string The user name for the DSN string.
    * @param string The password for the DSN string.
    * @see http://www.php.net/m...O-construct.php
    */
    public function __construct($dsn='',$username='',$password='',$slaveConfig=array())
    {
        $this->connectionString=$dsn;
        $this->username=$username;
        $this->password=$password;
        
        //设置从数据库配置
        $this->slaveConfig=$slaveConfig;
        
    }
    
    /**
    * Close the connection when serializing.
    */
    public function __sleep()
    {
        $this->close();
        return array_keys(get_object_vars($this));
    }
    
    /**
    * @return array list of available PDO drivers
    * @see http://www.php.net/m...ableDrivers.php
    */
    public static function getAvailableDrivers()
    {
        return PDO::getAvailableDrivers();
    }
    
    /**
    * Initializes the component.
    * This method is required by {@link IApplicationComponent} and is invoked by application
    * when the CDbConnection is used as an application component.
    * If you override this method, make sure to call the parent implementation
    * so that the component can be marked as initialized.
    */
    public function init()
    {
        parent::init();
        if($this->autoConnect)
            $this->setActive(true);
    }
    
    /**
    * @return boolean whether the DB connection is established
    */
    public function getActive()
    {
        return $this->_active;
    }
    
    /**
    * Open or close the DB connection.
    * @param boolean whether to open or close DB connection
    * @throws CException if connection fails
    */
    public function setActive($value)
    {
        //不在这边创建数据库链接，在getPdoInstance方法中创建，即在真正需要的时候才创建
        $this->_active=true;
        /*if($value!=$this->_active)
        {
        if($value)
        $this->open();
        else
        $this->close();
        }*/
    }
    
    /**
    * Opens DB connection if it is currently not
    * @throws CException if connection fails
    */
    protected function open($connectionString='', $username='', $password='')
    {
        //如果没有输入数据库配置，则使用主数据库配置
        $connectionString = $connectionString?$connectionString:$this->connectionString;
        $username = $username?$username:$this->username;
        $password = $password?$password:$this->password;
        
        if(empty($connectionString))
            throw new CDbException(Yii::t('yii','CDbConnection.connectionString cannot be empty.'));
        //本次创建出来的pdo链接
        $pdo = null;
        try
        {
            Yii::trace('Opening DB connection','system.db.CDbConnection');
            $pdo=$this->createPdoInstance($connectionString, $username, $password);
            $this->initConnection($pdo);
        }
        catch(PDOException $e)
        {
            if(YII_DEBUG)
            {
                throw new CDbException(Yii::t('yii','CDbConnection failed to open the DB connection: {error}',
                array('{error}'=>$e->getMessage())));
            }
            else
            {
                Yii::log($e->getMessage(),CLogger::LEVEL_ERROR,'exception.CDbException');
                throw new CDbException(Yii::t('yii','CDbConnection failed to open the DB connection.'));
            }
        }
        //返回创建的pdo链接
        return $pdo;
    }
    
    /**
    * Closes the currently active DB connection.
    * It does nothing if the connection is already closed.
    */
    protected function close()
    {
        Yii::trace('Closing DB connection','system.db.CDbConnection');
        $this->_pdo=null;
        $this->_active=false;
        $this->_schema=null;
        
        //删除主数据库链接、从数据库链接
        $this->_pdoMaster=null;
        $this->_pdoSlave=null;
    }
    
    /**
    * Creates the PDO instance.
    * When some functionalities are missing in the pdo driver, we may use
    * an adapter class to provides them.
    * @return PDO the pdo instance
    * @since 1.0.4
    */
    protected function createPdoInstance($connectionString, $username, $password)
    {
        $pdoClass='PDO';
        if(($pos=strpos($connectionString,':'))!==false)
        {
            $driver=strtolower(substr($this->connectionString,0,$pos));
            if($driver==='mssql' || $driver==='dblib')
                $pdoClass='CMssqlPdoAdapter';
        }
        return new $pdoClass($connectionString,$username,$password,$this->_attributes);
    }
    
    /**
    * Initializes the open db connection.
    * This method is invoked right after the db connection is established.
    * The default implementation is to set the charset for MySQL and PostgreSQL database connections.
    * @param PDO the PDO instance
    */
    protected function initConnection($pdo)
    {
        $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        if($this->emulatePrepare && constant('PDO::ATTR_EMULATE_PREPARES'))
            $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES,true);
        if($this->charset!==null)
        {
            if(strcasecmp($pdo->getAttribute(PDO::ATTR_DRIVER_NAME),'sqlite'))
                $pdo->exec('SET NAMES '.$pdo->quote($this->charset));
        }
        if($this->initSQLs!==null)
        {
            foreach($this->initSQLs as $sql)
            $pdo->exec($sql);
        }
    }
    
    /**
    * 真正的创建pdo链接
    * @return PDO the PDO instance, null if the connection is not established yet
    */
    public function getPdoInstance($useSlave=false)
    {
        //满足这两种情况才使用从库：1、存在从库；2、当前不处于一个事务中
        if( $useSlave &&
        count($this->slaveConfig) &&
        (!$this->_transaction || !$this->_transaction->getActive()))
        {
            if($this->_pdoSlave)
                $this->_pdo = $this->_pdoSlave;
            else
            {
                //随机选择一个从库
                $randIndex = array_rand($this->slaveConfig);
                extract($this->slaveConfig[$randIndex]);
                $this->_pdo = $this->_pdoSlave = $this->open($connectionString, $username, $password);
            }
        }
        else{
            if($this->_pdoMaster)
                $this->_pdo = $this->_pdoMaster;
            else
                //创建主数据库链接
                $this->_pdo = $this->_pdoMaster = $this->open();
        }
        return $this->_pdo;
    }
    
    /**
    * Creates a command for execution.
    * @param string SQL statement associated with the new command.
    * @return CDbCommand the DB command
    * @throws CException if the connection is not active
    */
    public function createCommand($sql)
    {
        if($this->getActive()){
            //需要使用CDbCommandExt类，因为该类可以获得sql的读写状态，从而可以选择主从数据库
            return new CDbCommandExt($this,$sql);
            //return new CDbCommand($this,$sql);
        }
        else
            throw new CDbException(Yii::t('yii','CDbConnection is inactive and cannot perform any DB operations.'));
    }
    
    /**
    * @return CDbTransaction the currently active transaction. Null if no active transaction.
    */
    public function getCurrentTransaction()
    {
        if($this->_transaction!==null)
        {
            if($this->_transaction->getActive())
            return $this->_transaction;
        }
        return null;
    }
    
    /**
    * Starts a transaction.
    * @return CDbTransaction the transaction initiated
    * @throws CException if the connection is not active
    */
    public function beginTransaction()
    {
        if($this->getActive())
        {
            //获得主数据库，将当前的pdo链接设置为主数据库
            $this->_pdo = $this->getPdoInstance();
            
            if($this->_pdo){
                $this->_pdo->beginTransaction();
                return $this->_transaction = new CDbTransaction($this);
            }
            else
                throw new CDbException(Yii::t('yii','CDbConnection is inactive and cannot perform any DB operations.'));
        }
        else
            throw new CDbException(Yii::t('yii','CDbConnection is inactive and cannot perform any DB operations.'));
    }
    
    /**
    * @return CDbSchema the database schema for the current connection
    * @throws CException if the connection is not active yet
    */
    public function getSchema()
    {
        if($this->_schema!==null)
            return $this->_schema;
        else
        {
            if(!$this->getActive())
                throw new CDbException(Yii::t('yii','CDbConnection is inactive and cannot perform any DB operations.'));
            $driver=$this->getDriverName();
            switch(strtolower($driver))
            {
                case 'pgsql': // PostgreSQL
                    return $this->_schema=new CPgsqlSchema($this);
                case 'mysqli': // MySQL
                case 'mysql':
                    return $this->_schema=new CMysqlSchema($this);
                case 'sqlite': // sqlite 3
                case 'sqlite2': // sqlite 2
                    return $this->_schema=new CSqliteSchema($this);
                case 'mssql': // Mssql driver on windows hosts
                case 'dblib': // dblib drivers on linux (and maybe others os) hosts
                    return $this->_schema=new CMssqlSchema($this);
                case 'oci': // Oracle driver
                    return $this->_schema=new COciSchema($this);
                case 'ibm':
                default:
                    throw new CDbException(Yii::t('yii','CDbConnection does not support reading schema for {driver} database.',
                    array('{driver}'=>$driver)));
            }
        }
    }
    
    /**
    * Returns the SQL command builder for the current DB connection.
    * @return CDbCommandBuilder the command builder
    * @since 1.0.4
    */
    public function getCommandBuilder()
    {
        return $this->getSchema()->getCommandBuilder();
    }
    
    /**
    * Returns the ID of the last inserted row or sequence value.
    * @param string name of the sequence object (required by some DBMS)
    * @return string the row ID of the last row inserted, or the last value retrieved from the sequence object
    * @see http://www.php.net/m...astInsertId.php
    */
    public function getLastInsertID($sequenceName='')
    {
        //必须使用主数据库链接
        if($this->getActive() && $this->_pdoMaster)
            //return $this->_pdo->lastInsertId($sequenceName);
            return $this->_pdoMaster->lastInsertId($sequenceName);
        else
            throw new CDbException(Yii::t('yii','CDbConnection is inactive and cannot perform any DB operations.'));
    }
    
    /**
    * Quotes a string value for use in a query.
    * @param string string to be quoted
    * @return string the properly quoted string
    * @see http://www.php.net/m...n.PDO-quote.php
    */
    public function quoteValue($str)
    {
        if($this->getActive()){
        //如果当前链接不存在，则创建一个与从数据库之间的链接
        if(!$this->_pdo)
            $this->_pdo = $this->getPdoInstance(true);
        
        //如果创建链接失败，抛出异常
        if(!$this->_pdo)
            throw new CDbException(Yii::t('yii','CDbConnection is inactive and cannot perform any DB operations.'));
        
            return $this->_pdo->quote($str);
        }
        else
            throw new CDbException(Yii::t('yii','CDbConnection is inactive and cannot perform any DB operations.'));
    }
    
    /**
    * Quotes a table name for use in a query.
    * @param string table name
    * @return string the properly quoted table name
    */
    public function quoteTableName($name)
    {
        return $this->getSchema()->quoteTableName($name);
    }
    
    /**
    * Quotes a column name for use in a query.
    * @param string column name
    * @return string the properly quoted column name
    */
    public function quoteColumnName($name)
    {
        return $this->getSchema()->quoteColumnName($name);
    }
    
    /**
    * Determines the PDO type for the specified PHP type.
    * @param string The PHP type (obtained by gettype() call).
    * @return integer the corresponding PDO type
    */
    public function getPdoType($type)
    {
        static $map=array
        (
        'boolean'=>PDO::PARAM_BOOL,
        'integer'=>PDO::PARAM_INT,
        'string'=>PDO::PARAM_STR,
        'NULL'=>PDO::PARAM_NULL,
        );
        return isset($map[$type]) ? $map[$type] : PDO::PARAM_STR;
    }
    
    /**
    * @return mixed the case of the column names
    * @see http://www.php.net/m...etattribute.php
    */
    public function getColumnCase()
    {
        return $this->getAttribute(PDO::ATTR_CASE);
    }
    
    /**
    * @param mixed the case of the column names
    * @see http://www.php.net/m...etattribute.php
    */
    public function setColumnCase($value)
    {
        $this->setAttribute(PDO::ATTR_CASE,$value);
    }
    
    /**
    * @return mixed how the null and empty strings are converted
    * @see http://www.php.net/m...etattribute.php
    */
    public function getNullConversion()
    {
        return $this->getAttribute(PDO::ATTR_ORACLE_NULLS);
    }
    
    /**
    * @param mixed how the null and empty strings are converted
    * @see http://www.php.net/m...etattribute.php
    */
    public function setNullConversion($value)
    {
        $this->setAttribute(PDO::ATTR_ORACLE_NULLS,$value);
    }
    
    /**
    * @return boolean whether creating or updating a DB record will be automatically committed.
    * Some DBMS (such as sqlite) may not support this feature.
    */
    public function getAutoCommit()
    {
        return $this->getAttribute(PDO::ATTR_AUTOCOMMIT);
    }
    
    /**
    * @param boolean whether creating or updating a DB record will be automatically committed.
    * Some DBMS (such as sqlite) may not support this feature.
    */
    public function setAutoCommit($value)
    {
        $this->setAttribute(PDO::ATTR_AUTOCOMMIT,$value);
    }
    
    /**
    * @return boolean whether the connection is persistent or not
    * Some DBMS (such as sqlite) may not support this feature.
    */
    public function getPersistent()
    {
        return $this->getAttribute(PDO::ATTR_PERSISTENT);
    }
    
    /**
    * @param boolean whether the connection is persistent or not
    * Some DBMS (such as sqlite) may not support this feature.
    */
    public function setPersistent($value)
    {
        return $this->setAttribute(PDO::ATTR_PERSISTENT,$value);
    }
    
    /**
    * @return string name of the DB driver
    */
    public function getDriverName()
    {
        return $this->getAttribute(PDO::ATTR_DRIVER_NAME);
    }
    
    /**
    * @return string the version information of the DB driver
    */
    public function getClientVersion()
    {
        return $this->getAttribute(PDO::ATTR_CLIENT_VERSION);
    }
    
    /**
    * @return string the status of the connection
    * Some DBMS (such as sqlite) may not support this feature.
    */
    public function getConnectionStatus()
    {
        return $this->getAttribute(PDO::ATTR_CONNECTION_STATUS);
    }
    
    /**
    * @return boolean whether the connection performs data prefetching
    */
    public function getPrefetch()
    {
        return $this->getAttribute(PDO::ATTR_PREFETCH);
    }
    
    /**
    * @return string the information of DBMS server
    */
    public function getServerInfo()
    {
        return $this->getAttribute(PDO::ATTR_SERVER_INFO);
    }
    
    /**
    * @return string the version information of DBMS server
    */
    public function getServerVersion()
    {
        return $this->getAttribute(PDO::ATTR_SERVER_VERSION);
    }
    
    /**
    * @return int timeout settings for the connection
    */
    public function getTimeout()
    {
        return $this->getAttribute(PDO::ATTR_TIMEOUT);
    }
    
    /**
    * Obtains a specific DB connection attribute information.
    * @param int the attribute to be queried
    * @return mixed the corresponding attribute information
    * @see http://www.php.net/m...etAttribute.php
    */
    public function getAttribute($name)
    {
        if($this->getActive()){
        //如果当前链接不存在，则创建一个与从数据库之间的链接
        if(!$this->_pdo)
        $this->_pdo = $this->getPdoInstance(true);
        
        //如果创建链接失败，抛出异常
        if(!$this->_pdo)
        throw new CDbException(Yii::t('yii','CDbConnection is inactive and cannot perform any DB operations.'));
        
        return $this->_pdo->getAttribute($name);
        }
        else
        throw new CDbException(Yii::t('yii','CDbConnection is inactive and cannot perform any DB operations.'));
    }
    
    /**
    * Sets an attribute on the database connection.
    * @param int the attribute to be set
    * @param mixed the attribute value
    * @see http://www.php.net/m...etAttribute.php
    */
    public function setAttribute($name,$value)
    {
        if($this->_pdo instanceof PDO)
        $this->_pdo->setAttribute($name,$value);
        /*else
        $this->_attributes[$name]=$value;*/
        //记录设置数据库属性，在主库与从库之间切换时，数据库属性需要从新设置
        $this->_attributes[$name]=$value;
    }
    
    /**
    * Returns the statistical results of SQL executions.
    * The results returned include the number of SQL statements executed and
    * the total time spent.
    * In order to use this method, {@link enableProfiling} has to be set true.
    * @return array the first element indicates the number of SQL statements executed,
    * and the second element the total time spent in SQL execution.
    * @since 1.0.6
    */
    public function getStats()
    {
        $logger=Yii::getLogger();
        $timings=$logger->getProfilingResults(null,'system.db.CDbCommand.query');
        $count=count($timings);
        $time=array_sum($timings);
        $timings=$logger->getProfilingResults(null,'system.db.CDbCommand.execute');
        $count+=count($timings);
        $time+=array_sum($timings);
        return array($count,$time);
    }
} 